## Copyright (c) 2008 Bartlett Publishing

## This class describes the basic machine registers and internal functions
## which are used by the derived machines.  Usually you derive a machine
## to setup/modify an instruction set (DefaultMachine is the best current
## instruction set).  But you can also override some core internal 
## functionality.
class Machine
  def self.define_instruction(instr_name, opts = {}, &block)
    @instructions ||= {}
    full_instr_name = "instruction_#{instr_name}".to_sym
    instr_name = instr_name.to_sym

    instr_info = {
      :name => instr_name,
      :method => full_instr_name,
      #DEFAULTS
      :cycles => 1,
      :redundant => 1,
      :parameters => []
    }.merge(opts)
    @instructions[instr_name] = instr_info
    
    if block.nil?
      define_method full_instr_name do
      end
    else
      define_method full_instr_name, &block
    end
  end

  def self.remove_instruction(instr_name)
    #NOTE - this must set it to nil instead of delete it
    #       because we can inherit instructions from below
    @instructions ||= {}
    @instructions[instr_name] = nil
  end

  def self.instructions_this_level
    @instructions || {}
  end

  def save_score(score)
    @score = score
  end

  def score
    @score
  end

  def scores
    [@score] + (@parent.nil? ? [] : @parent.scores)
  end

  def self.instructions
    @all_instructions ||= begin
      current_instructions = instructions_this_level
      if self != Machine
        remaining_instructions = superclass.instructions
      else
        remaining_instructions = {}
      end

      remaining_instructions.merge(current_instructions)
    end
  end

  def self.reload_full_instruction_list
    @all_instructions = nil
  end
  
  def instructions
    self.class.instructions
  end

  def initialize(instructions = [], parent = nil)
    @program_instructions = instructions
    @parent = parent
    reset_computation
  end

  def dup
    self.class.new(@program_instructions, self)
  end

  #NOTE - opts is not currently used
  #NOTE - starts at the current instruction pointer and works out, finding the best marker match, but I may want to penalize for distance
  #NOTE - it might be fun to do a stochastic jump marker - i.e. find the closest one 30%, the next one out 20% of the time, the next one out 15% of the time, etc.
  def find_jump_marker(val, opts = {})
    worst_marker_match = 10  #FIXME - constant - needs to be looked at
    max_ip = @program_instructions.size - 1
    min_ip = 0
    max_count = @program_instructions.size
    best_marker_idx = nil
    best_marker_match = nil
    1.upto(max_count) do |count|
      left_ip = @instruction_pointer - count
      right_ip = @instruction_pointer + count
      [left_ip, right_ip].each do |cur_ip|
        instr = @program_instructions[cur_ip]
        unless instr.nil?
          if instr[0] == :marker
            marker_match = (instr[1] - val).abs
            if marker_match < worst_marker_match
              if best_marker_idx.nil?
                best_marker_idx = cur_ip
                best_marker_match = marker_match
              else
                if marker_match < best_marker_match
                  best_marker_match = marker_match
                  best_marker_idx = cur_ip
                end
              end
            end
          end
        end
      end
    end

    return best_marker_idx || (opts[:nil_if_not_found] ? nil : @instruction_pointer)
  end


  def load_program(program)
    @program_instructions = program
  end

  def program_instructions
    @program_instructions
  end

  def registers
    @registers
  end

  def stacks
    @stacks
  end

  def remaining_cycles
    @remaining_cycles
  end

  def self.normalize_program_instructions(program)
    program.map{|x| Array === x ? x : [x]}
  end

  def reset_computation
    @stacks = []
    @registers = []
    @instruction_pointer = 0
    @remaining_cycles = 1000
  end

  def run_computation(opts = {})
    while(true) do
      return :no_more_cycles if @remaining_cycles == 0

      next_instruction = @program_instructions[@instruction_pointer]
      return :end_of_instructions if next_instruction.nil?
      return :explicit_break if next_instruction[0] == :break

      puts "#{next_instruction.inspect}: #{@remaining_cycles}" if opts[:verbose]
      run_instruction(*next_instruction)
    end
  end

  def run_instruction(instr, *args)
    instruction = self.instructions[instr]

    if instruction.nil?
      throw "Invalid Instruction: #{instr}"
    end

    self.send(instruction[:method], args)

    @instruction_pointer = @instruction_pointer + 1    
    @remaining_cycles = @remaining_cycles - instruction[:cycles]
  end

  def available_mutation_types
    [:parameter_change, :insertion, :deletion]
  end

  def mutate(opts = {})
    opts = {
      :max_mutations => 5,
      :mutation_types => available_mutation_types
    }.merge(opts)

    mutation_types = opts[:mutation_types]
    num_mutation_types = mutation_types.size
    num_mutations = rand(opts[:max_mutations]) + 1

    1.upto(num_mutations) do
      mutation_type = mutation_types[rand(num_mutation_types)]
      self.send("mutate_#{mutation_type}", opts)
    end

    return self #make it easier to do more stuff with
  end

  def mutate_parameter_change(opts = {})
    if @program_instructions.size > 0
      instr_idx = rand(@program_instructions.size)
      instr = @program_instructions[instr_idx]
      instr_name = instr[0]
      instr_info = instructions[instr_name]
      modify_instruction(instr_info, instr, opts = {})
    end
  end

  def mutate_insertion(opts = {})
    opts = {
      :max_insertion_size => 4
    }.merge(opts)

    insertion_point = rand(@program_instructions.size)
    insertion_size = rand(opts[:max_insertion_size])
    insertions = (1..insertion_size).map do 
      instr_idx = rand(instructions.size)
      new_instr_info = instructions.values[instr_idx]
      generate_instruction(new_instr_info)
    end
    @program_instructions.insert(insertion_point, *insertions)
  end

  def generate_instruction(info)
    params = info[:parameters].map do |param_info|
      generate_value_for_parameter(param_info)
    end
    params.unshift(info[:name])
  end

  def generate_value_for_parameter(param_info)
    case param_info[:type]
      when :integer
        case param_info[:range]
          when Range:
            rinfo = param_info[:range]
            r = rinfo.end - rinfo.begin
            v = rand(r) + rinfo.begin
            return v
          else
            puts "WARNING: Parameter Range Type Not Found: #{param_info.inspect}"
        end
      else
        puts "WARNING: Parameter Type Not Found: #{param_info[:type].inspect}"
    end
  end

  def modify_instruction(info, instr, opts = {})
    info[:parameters].each_index do |pidx|
      if rand(2) == 1
        instr[pidx + 1] = generate_value_for_parameter(info[:parameters][pidx])
      end
    end
  end

  def mutate_deletion(opts = {})
    opts = {
      :max_deletion_size => 2
    }.merge(opts)

    deletion_size = rand(opts[:max_deletion_size])
    num_instructions = @program_instructions.size
    deletion_size = 1 if deletion_size >= num_instructions

    deletion_start = rand(num_instructions - deletion_size - 1)

    @program_instructions.slice!(deletion_start, deletion_size)
  end

  def self.default_register_range
    0..5
  end

  def self.machine_register_type
    { :type => :integer, :range => default_register_range }
  end
 
  def self.default_stack_range 
    0..5
  end

  def self.machine_stack_type
    { :type => :integer, :range => default_stack_range }
  end

  def self.default_integer_range
    0..100
  end

  def self.machine_integer_type
    { :type => :integer, :range => default_integer_range }
  end

  def self.default_float_range
    0.0..100.0
  end

  def self.machine_float_type
    { :type => :float, :range => default_float_range }
  end

end
